/**
 * 
 */
package jmine.tec.utils.io;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.Closeable;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInput;
import java.io.ObjectInputStream;
import java.io.ObjectOutput;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.io.Reader;
import java.io.Writer;
import java.util.Collection;

import org.apache.commons.logging.LogFactory;

/**
 * Utilitários para I/O
 * 
 * @author gigante
 */
public final class IOUtils {

    public static final int BUFFER_SIZE = 1024;

    /**
     * Construtor privado.
     */
    private IOUtils() {
        super();
    }

    /**
     * Lê de um stream inteiro, devolvendo o conteudo e fechando a stream.
     * 
     * @param input {@link InputStream}
     * @throws IOException IOException
     * @return byte[]
     */
    public static byte[] readFully(final InputStream input) throws IOException {
        ByteArrayOutputStream ret = new ByteArrayOutputStream();
        copyFullyClose(input, ret);
        return ret.toByteArray();
    }

    /**
     * Le um Reader ateh o final
     * 
     * @param reader {@link Reader}
     * @return String
     * @throws IOException e
     */
    public static String readFullyClose(Reader reader) throws IOException {
        try {
            StringBuilder sb = new StringBuilder();
            int read = reader.read();
            while (read >= 0) {
                sb.append((char) read);
                read = reader.read();
            }
            return sb.toString();
        } finally {
            reader.close();
        }
    }

    /**
     * Copy the content from the InputStream into a temporary file that will be deleted when this JVM exits normally
     * 
     * @param pref the preffix
     * @param suffix the suffix
     * @param source the source {@link InputStream}
     * @return {@link File}
     * @throws IOException if an IOException occurs
     */
    public static File copyToTempFile(final String pref, final String suffix, final InputStream source) throws IOException {
        return copyToTempFile(pref, suffix, null, source);
    }

    /**
     * Copy the content from the InputStream into a temporary file that will be deleted when this JVM exits normally
     * 
     * @param pref the preffix
     * @param suffix the suffix
     * @param parent the parent folder
     * @param source the inputStream
     * @return File
     * @throws IOException if an iooexception occurs
     */
    public static File copyToTempFile(final String pref, final String suffix, final File parent, final InputStream source)
            throws IOException {
        File file = File.createTempFile(pref, suffix, parent);
        IOUtils.copyFullyClose(source, new FileOutputStream(file));
        file.deleteOnExit();
        return file;

    }

    /**
     * Copy the bytes fully from the source into the output, closing both streams in the end.
     * 
     * @param source the source input stream
     * @param output the output
     * @throws IOException if an IOException occurs, either reading, writing or closing any of the streams.
     */
    public static void copyFullyClose(final InputStream source, final OutputStream output) throws IOException {
        try {
            copyFully(source, output);
        } finally {
            try {
                source.close();
            } finally {
                output.close();
            }
        }
    }

    /**
     * Converte um objeto do tipo {@link ObjectInput} para um {@link InputStream}
     * 
     * @param objectInput {@link ObjectInput}
     * @return {@link InputStream}
     */
    public static InputStream adaptObjectInput(final ObjectInput objectInput) {
        if (objectInput instanceof InputStream) {
            return ((InputStream) objectInput);
        }
        return new InputStream() {

            @Override
            public int read() throws IOException {
                return objectInput.read();
            }

            @Override
            public int read(byte[] b, int off, int len) throws IOException {
                return objectInput.read(b, off, len);
            }

            @Override
            public void close() throws IOException {
                objectInput.close();
            }

        };
    }

    /**
     * Converte um objeto do tipo {@link ObjectOutput} para um {@link OutputStream}
     * 
     * @param objectOutput {@link ObjectOutput}
     * @return {@link OutputStream}
     */
    public static OutputStream adaptObjectOutput(final ObjectOutput objectOutput) {
        if (objectOutput instanceof OutputStream) {
            return (OutputStream) objectOutput;
        }
        return new OutputStream() {

            @Override
            public void write(int b) throws IOException {
                objectOutput.write(b);
            }

            @Override
            public void write(byte[] b, int off, int len) throws IOException {
                objectOutput.write(b, off, len);
            }

            @Override
            public void close() throws IOException {
                objectOutput.close();
            }

        };
    }

    /**
     * Copy the bytes fully from the source into the output, closing both streams in the end.
     * 
     * @param source the source input stream
     * @param output the output
     * @return the total amount of bytes copied
     * @throws IOException if an IOException occurs, either reading, writing or closing any of the streams.
     */
    public static long copyFullyCloseGetTotal(final InputStream source, final OutputStream output) throws IOException {
        try {
            return copyFullyGetTotal(source, output);
        } finally {
            try {
                source.close();
            } finally {
                output.close();
            }
        }
    }

    /**
     * Copy the bytes fully from the source into the output.
     * 
     * @param source the source input stream
     * @param output the output
     * @throws IOException if an IOException occurs, either reading, writing or closing any of the streams.
     */
    public static void copyFully(final InputStream source, final OutputStream output) throws IOException {
        copyFullyGetTotal(source, output);
    }

    /**
     * Copy the bytes fully from the source into the output.
     * 
     * @param source the source input stream
     * @param output the output
     * @return the amount of bytes copied
     * @throws IOException if an IOException occurs, either reading, writing or closing any of the streams.
     */
    public static long copyFullyGetTotal(final InputStream source, final OutputStream output) throws IOException {
        byte[] buffer = new byte[BUFFER_SIZE];
        long total = 0;
        int read = -1;
        while ((read = source.read(buffer)) >= 0) {
            output.write(buffer, 0, read);
            total += read;
        }
        return total;
    }

    /**
     * Copy the bytes fully from the source into the output.
     * 
     * @param source the source input stream
     * @param output the output
     * @return the amount of bytes copied
     * @throws IOException if an IOException occurs, either reading, writing or closing any of the streams.
     */
    public static long copyFullyGetTotal(final Reader source, final Writer output) throws IOException {
        char[] buffer = new char[BUFFER_SIZE / 2];
        long total = 0;
        int read = -1;
        while ((read = source.read(buffer)) >= 0) {
            output.write(buffer, 0, read);
            total += read;
        }
        return total;
    }

    /**
     * Copy the bytes fully from the source into the output.
     * 
     * @param source the source input stream
     * @param output the output
     * @return the amount of bytes copied
     * @throws IOException if an IOException occurs, either reading, writing or closing any of the streams.
     */
    public static long copyFullyClose(final Reader source, final Writer output) throws IOException {
        try {
            return copyFullyGetTotal(source, output);
        } finally {
            try {
                source.close();
            } finally {
                output.close();
            }
        }
    }

    /**
     * Serializa um {@link Object} em um array de bytes
     * 
     * @param serializable o objeto a ser serializado
     * @return byte[]
     */
    public static byte[] serializeObject(final Object serializable) {
        try {
            final ByteArrayOutputStream bout = new ByteArrayOutputStream();
            ObjectOutputStream out = new ObjectOutputStream(bout);
            out.writeObject(serializable);
            out.flush();
            out.close();
            return bout.toByteArray();
        } catch (IOException e) {
            throw new IllegalStateException("unexpected IOException", e);
        }
    }

    /**
     * Le um objeto serializado como um array de bytes
     * 
     * @param array array de bytes
     * @return Object
     * @throws ClassNotFoundException se a classe nao for encontrada no classloader
     */
    public static Object deserializeObject(final byte[] array) throws ClassNotFoundException {
        try {
            ObjectInputStream oin = new ObjectInputStream(new ByteArrayInputStream(array));
            return oin.readObject();
        } catch (IOException e) {
            throw new IllegalStateException("unexpected IOException", e);
        }
    }

    /**
     * Clona um objeto, serializando. Devolve <code>null</code> se <code>null</code> for passado
     * 
     * @param serializable o objeto a ser clonado
     * @return objeto clonado.
     * @param <T> o tipo do objeto
     */
    @SuppressWarnings("unchecked")
    public static <T> T deepClone(final T serializable) {
        if (serializable == null) {
            return null;
        }
        try {
            return (T) deserializeObject(serializeObject(serializable));
        } catch (ClassNotFoundException e) {
            throw new IllegalStateException("Excecao inesperada!!", e);
        }
    }

    /**
     * Faz um 'append' entre os input streams
     * 
     * @param inputStreams array de inputStreams
     * @return um inputStream
     */
    public static InputStream append(final InputStream... inputStreams) {
        return new InputStream() {

            private final InputStream[] inputs = inputStreams;

            private int index;

            private boolean isFinished() {
                return this.index >= this.inputs.length;
            }

            @Override
            public synchronized int read() throws IOException {
                if (this.isFinished()) {
                    return -1;
                }
                int next = this.inputs[this.index].read();
                if (next == -1) {
                    this.index++;
                    return this.read();
                }
                return next;
            }

            @Override
            public synchronized int read(byte[] b, int off, int len) throws IOException {
                if (this.isFinished()) {
                    return -1;
                }
                int n = this.inputs[this.index].read(b, off, len);
                if (n == -1) {
                    this.index++;
                    return this.read(b, off, len);
                }
                return n;
            }

            @Override
            public void close() throws IOException {
                IOException ex = null;
                for (InputStream is : this.inputs) {
                    try {
                        is.close();
                    } catch (IOException e) {
                        ex = e;
                    }
                }
                if (ex != null) {
                    throw ex;
                }
            }

        };
    }

    /**
     * Fecha o {@link Closeable} passado, logando o eventual {@link IOException}. Demais excecoes nao checadas sao propagadas normalmente.
     * 
     * @param close {@link Closeable}
     * @param callingClass {@link Class}
     */
    public static void closeResourceLogException(Closeable close, Class<?> callingClass) {
        try {
            close.close();
        } catch (IOException e) {
            LogFactory.getLog(callingClass).warn("IOException while closing resource", e);
        }
    }

    /**
     * Fecha todos os closeables, ignorando eventuais excecoes
     * 
     * @param deferred List of Closeable
     * @param callingClass the calling class
     */
    public static void closeAllIgnoreExceptions(Collection<? extends Closeable> deferred, Class<?> callingClass) {
        for (Closeable closeable : deferred) {
            closeResourceLogException(closeable, callingClass);
        }
    }

}
